JSON.stringify는 순환참조 객체를 지원하지 못하고 예외를 던진다.
문제현상
axios
에서 예외가 발생함- 해당 예외 객체를
stringifyError()
메서드에 전달하여 로그로 출력하려고 시도함 - 하지만
axios
의 에러 객체는 내부적으로 순환참조(circular reference) 를 포함하고 있음 JSON.stringify()
는 기본적으로 순환참조가 있는 객체를 직렬화할 수 없기 때문에 예외(TypeError: Converting circular structure to JSON
)를 발생시킴
private stringifyError(error: unknown): string {
if (error instanceof Error) {
const details: Record<string, unknown> = {
name: error.name,
message: error.message,
stack: error.stack,
};
for (const key of Object.getOwnPropertyNames(error)) {
if (!(key in details)) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(details as any)[key] = (error as any)[key];
}
}
return JSON.stringify(details); // <-- 💀
}
try {
return JSON.stringify(error);
} catch {
return String(error);
}
}
원인분석
written by Chat GPT 4o
-
axios
의 에러 객체는Error
클래스를 상속한 객체이지만, 여기에 추가된 필드들 (config
,request
,response
) 중 일부는 다시 자신을 참조하는 구조를 가짐 → 즉 객체 내부에 순환 링크가 존재 -
예:
error.config.transformRequest[0] === circular function
, 혹은error.response.config === error.config
해결 방법
1. json-stringify-safe
사용 (추천)
npm install json-stringify-safe
import stringifySafe from 'json-stringify-safe';
return stringifySafe(details);
json-stringify-safe
는 순환참조가 감지되면[Circular]
로 대체하여 문자열을 생성함
2. 수동 순환 제거
config
, request
, response
등을 제거하거나 최소한의 필드만 추출하여 별도 처리
const cleanedError = {
name: error.name,
message: error.message,
stack: error.stack,
isAxiosError: (error as any).isAxiosError,
code: (error as any).code,
url: (error as any)?.config?.url,
method: (error as any)?.config?.method,
status: (error as any)?.response?.status,
};
return JSON.stringify(cleanedError);
3. 에러 객체 로깅 전 fallback 처리
try {
return JSON.stringify(details);
} catch (e) {
return `Unserializable error object: ${String(error)}`;
}
요약
-
JSON.stringify()
는 순환참조를 포함한 객체에 대해 동작하지 않으며 예외를 발생시킴 -
axios
에러 객체는 순환참조 구조를 가질 수 있으므로 직렬화 전에 안전 조치 필요 -
해결책으로는
json-stringify-safe
모듈 사용 또는 필요한 속성만 추출하여 순환 제거 방식이 있음
NodeJS 환경에서 util.inspect 함수를 써도 된다.
역직렬화는 안되지만 콘솔 출력시 객체를 문자열로 변환할때 쓰는 좋은 함수이다.
참조: https://nodejs.org/api/util.html#utilinspectobject-options
위에서 걱정했던 순환참조도 알아서 [Circular]
로 감싸준다
const { inspect } = require('node:util');
const obj = {};
obj.a = [obj];
obj.b = {};
obj.b.inner = obj.b;
obj.b.obj = obj;
console.log(inspect(obj));
// <ref *1> {
// a: [ [Circular *1] ],
// b: <ref *2> { inner: [Circular *2], obj: [Circular *1] }
// }